﻿/*
Copyright (c) 2013, SonicLing
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

    Redistributions of source code must retain the above copyright notice, this 
list of conditions and the following disclaimer.
    Redistributions in binary form must reproduce the above copyright notice, 
this list of conditions and the following disclaimer in the documentation and/or 
other materials provided with the distribution.
    Neither the name of the SonicLing nor the names of its contributors may be 
used to endorse or promote products derived from this software without specific 
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Reflection;

namespace Bibtex2Word
{
	public interface IItemSetter
	{
		void SetItem(WordBibSource source, string name, object value);
	}
	public class BibTranslator
	{
		public Dictionary<string, IItemSetter> translationTable = new Dictionary<string, IItemSetter>();
		public BibTranslator()
		{
			// see https://en.wikipedia.org/wiki/BibTeX
			translationTable.Add("type", new SourceTypeSetter());
			translationTable.Add("tag", new FieldSetter());
			translationTable.Add("guid", new FieldSetter());

			translationTable.Add("author", new AuthorSetter());
			translationTable.Add("title", new FieldSetter());
			translationTable.Add("journal", new BindedFieldSetter("JournalName"));
			translationTable.Add("booktitle", new MultiBindedFieldSetter("BookTitle", "ConferenceName"));
			translationTable.Add("series", new BindedFieldSetter("ShortTitle"));
			translationTable.Add("volume", new FieldSetter());
			translationTable.Add("issue", new FieldSetter());
			translationTable.Add("city", new FieldSetter());
			translationTable.Add("location", new BindedFieldSetter("City"));
			translationTable.Add("pages", new FieldSetter());
			translationTable.Add("publisher", new FrontMergeSetter("Publisher"));
			translationTable.Add("address", new BackMergeSetter("Publisher", ", "));
			translationTable.Add("issn", new FrontMergeSetter("StandardNumber", "/"));
			translationTable.Add("isbn", new BackMergeSetter("StandardNumber", "/"));
			translationTable.Add("year", new FieldSetter());
			translationTable.Add("month", new FieldSetter());
			translationTable.Add("day", new FieldSetter());
			translationTable.Add("issue_date", new IssueDateSetter());
			translationTable.Add("affiliation", new BindedFieldSetter("Institution"));
			translationTable.Add("school", new BindedFieldSetter("Institution"));

		}

		public WordBibSource[] Translate(PathItem file)
		{
			if (file.IsExisted)
				return Translate(System.IO.File.ReadAllText(file.Path));
			else
				return new WordBibSource[] { };
		}

		public const string bibitem = @"@(?<type>\w+)\s*\{(?<tag>[^,]+),\s*((?<name>[a-zA-Z0-9_]+)\s*=\s*([\{\x22](?<value>([^\{\}\x22\r\n]|((?'open'\{[^\{\}]*)+(?'close-open'\}[^\{\}]*)+))*)[\}\x22]|(?<value>[^,\r\n]*))\s*,?\s*)+,?\}";
		public WordBibSource[] Translate(string content)
		{
			Regex regex = new Regex(bibitem);
			MatchCollection mc = regex.Matches(content);
			int index = 0;
			List<WordBibSource> Sources = new List<WordBibSource>();
			foreach (Match m in mc)
			{
				WordBibSource source = new WordBibSource();
				System.Diagnostics.Trace.WriteLine(string.Format("match {0}", index++));
				string stype = m.Groups["type"].Captures[0].Value;
				string tag = m.Groups["tag"].Captures[0].Value;
				//System.Diagnostics.Trace.WriteLine(string.Format("\ttype = {0}", stype));
				//System.Diagnostics.Trace.WriteLine(string.Format("\ttag = {0}", tag));
				translationTable["type"].SetItem(source, "type", stype);
				translationTable["tag"].SetItem(source, "tag", tag);
				translationTable["guid"].SetItem(source, "guid", Guid.NewGuid());

				for (int i = 0; i < m.Groups["name"].Captures.Count && i < m.Groups["value"].Captures.Count; i++)
				{
					string name = m.Groups["name"].Captures[i].Value;
					string value = m.Groups["value"].Captures[i].Value;
					//System.Diagnostics.Trace.WriteLine(string.Format("\t\t[{0}] = <{1}>", name, value));
					if (string.IsNullOrWhiteSpace(value))
					{
						// do nothing
					}
					else if (translationTable.ContainsKey(name))
					{
						translationTable[name].SetItem(source, name, value);
					}
					else
					{
						System.Diagnostics.Trace.TraceWarning("item [{0}]=<{1}> is not included in Word Bibliography, please check.", name, value);
					}
				}
				Sources.Add(source);
			}
			return Sources.ToArray();
		}
	}

	class SourceTypeSetter : IItemSetter
	{
		public void SetItem(WordBibSource source, string name, object value)
		{
			switch (value.ToString().ToLower())
			{
				case "article":
					source.SourceType = SourceTypes.JournalArticle;
					break;
				case "conference":
				case "proceedings":
				case "inproceedings":
					source.SourceType = SourceTypes.ConferenceProceedings;
					break;
				case "inbook":
				case "incollection":
					source.SourceType = SourceTypes.BookSection;
					break;
				case "book":
				case "booklet":
					source.SourceType = SourceTypes.Book;
					break;
				case "misc":
					source.SourceType = SourceTypes.Misc;
					break;
				case "mastersthesis":
				case "phdthesis":
					source.SourceType = SourceTypes.Art;
					break;
				case "techreport":
					source.SourceType = SourceTypes.Report;
					break;
				default:
					throw new Exception(string.Format("{0} is unknown source type.", value));
			}
		}
	}

	class AuthorSetter : IItemSetter
	{
		public void SetItem(WordBibSource source, string name, object value)
		{
			// author and author ...
			string authors = value.ToString();
			string[] authorlist = authors.Split(new string[] { " and " }, StringSplitOptions.RemoveEmptyEntries);

			source.Author = new WordBibAuthor();
			WordBibNameList nameList = new WordBibNameList();
			switch (source.SourceType)
			{
				case SourceTypes.Art:
					source.Author.Artist = nameList;
					break;
				case SourceTypes.Interview:
					source.Author.Interviewee = nameList;
					break;
				case SourceTypes.Patent:
					source.Author.Inventor = nameList;
					break;
				default:
					source.Author.Author = nameList;
					break;
			}
			nameList.NameList = new WordBibPersons();
			nameList.NameList.Person = new List<WordBibPerson>();

			foreach (string author in authorlist)
			{
				// first (middle) last
				// last, (middle) first
				WordBibPerson person = new WordBibPerson();
				string[] names = author.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
				if (names.Length == 1)
				{
					// first version
					string[] names2 = names[0].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
					if (names2.Length > 0)
					{
						if (names2.Length > 1)
						{
							person.First = FormatName(names2[0]);
						}
						person.Last = FormatName(names2[names2.Length - 1]);
					}
					if (names2.Length > 2)
					{
						StringBuilder middle = new StringBuilder();
						for (int i = 1; i < names2.Length - 1; i++)
						{
							middle.AppendFormat("{0} ", names2[i]);
						}
						person.Middle = FormatName(middle.ToString());
					}
				}
				else if (names.Length > 1)
				{
					// second-third version
					person.Last = FormatName(names[0]);
					string[] names2 = names[1].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
					if (names2.Length > 1)
					{
						person.Middle = FormatName(names2[0]);
						person.First = FormatName(names2[1]);
					}
					else if (names2.Length > 0)
					{
						person.First = FormatName(names2[0]);
					}
				}
				nameList.NameList.Person.Add(person);
			}
		}

		private string FormatName(string name)
		{
			string[] uncapitalized = new string[]
			{
				"de", "la", "von"
			};
			// e.g. DE LA CAMPER => De La Camper
			// FIX ME: dealing with accented character such as \'a, \'\i, ...
			string[] fragments = name.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			StringBuilder builder = new StringBuilder();
			foreach (string fragment in fragments)
			{
				if (uncapitalized.Any(s => s.Equals(fragment.ToLower())))
				{
					builder.AppendFormat("{0} ", fragment);
				}
				else
				{
					string init = "", tail = "";
					if (fragment.Length > 0)
					{
						init = fragment[0].ToString();
					}
					if (fragment.Length > 1)
					{
						tail = fragment.Substring(1);
					}
					builder.Append(string.Format("{0}{1} ", init.ToUpper(), tail.ToLower()));
				}
			}
			return builder.ToString().Trim();
		}
	}

	class IssueDateSetter : IItemSetter
	{
		public void SetItem(WordBibSource source, string name, object value)
		{
			string[] dates = value.ToString().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string i in dates)
			{
				if (char.IsDigit(i[0]))
				{
					int n = int.Parse(i);
					if (n > 31)
					{
						source.YearAccessed = i;
						if (string.IsNullOrEmpty(source.Year))
						{
							source.Year = i;
						}
					}
					else
					{
						source.DayAccessed = i;
					}
				}
				else
				{
					source.MonthAccessed = i;
				}
			}
		}
	}

	class FieldSetter : IItemSetter
	{
		public virtual void SetItem(WordBibSource source, string name, object value)
		{
			FieldInfo field = typeof(WordBibSource).GetField(name, BindingFlags.Public|BindingFlags.Instance|BindingFlags.IgnoreCase);
			if (field != null)
			{
				if (field.FieldType.Equals(typeof(string)))
				{
					object original = field.GetValue(source);
					if (original == null)
					{
						field.SetValue(source, value);
					}
					else
					{
						field.SetValue(source, GetValue(original.ToString(), value.ToString()));
					}
				}
				else if (field.FieldType.Equals(value.GetType()))
				{
					field.SetValue(source, value);
				}
				else
				{
					throw new Exception(string.Format("WordBibSource.{0} is not string type", name));
				}
			}
			else
			{
				throw new Exception(string.Format("WordBibSource has no field named {0}.", name));
			}
		}

		protected virtual string GetValue(string original, string value)
		{
			return value;
		}
	}
	
	class BindedFieldSetter : FieldSetter
	{
		public readonly string Field;
		public BindedFieldSetter(string field)
		{
			Field = field;
		}
		public override void SetItem(WordBibSource source, string name, object value)
		{
			base.SetItem(source, Field, value);
		}
	}

	class FrontMergeSetter : BindedFieldSetter
	{
		public readonly string Splitter;
		public FrontMergeSetter(string field, string splitter = " ")
			: base(field)
		{
			Splitter = splitter;
		}
		protected override string GetValue(string original, string value)
		{
			return string.Format("{0}{1}{2}", value, Splitter, original);
		}
	}

	class BackMergeSetter : BindedFieldSetter
	{
		public readonly string Splitter;
		public BackMergeSetter(string field, string splitter = " ")
			: base(field)
		{
			Splitter = splitter;
		}
		protected override string GetValue(string original, string value)
		{
			return string.Format("{0}{1}{2}", original, Splitter, value);
		}
	}

	class MultiBindedFieldSetter : FieldSetter
	{
		public readonly string[] Field;
		public MultiBindedFieldSetter(params string[] field)
		{
			Field = field;
		}
		public override void SetItem(WordBibSource source, string name, object value)
		{
			foreach (string field in Field)
			{
				base.SetItem(source, field, value);
			}
		}
	}
}
